@@ -5,13 +5,21 @@ module FileHandling |
||
5 | 5 |
{ file_pointer: { file: file, agent_id: id } } |
6 | 6 |
end |
7 | 7 |
|
8 |
+ def has_file_pointer?(event) |
|
9 |
+ event.payload['file_pointer'] && |
|
10 |
+ event.payload['file_pointer']['file'] && |
|
11 |
+ event.payload['file_pointer']['agent_id'] |
|
12 |
+ end |
|
13 |
+ |
|
8 | 14 |
def get_io(event) |
9 |
- return nil unless event.payload['file_pointer'] && |
|
10 |
- event.payload['file_pointer']['file'] && |
|
11 |
- event.payload['file_pointer']['agent_id'] |
|
15 |
+ return nil unless has_file_pointer?(event) |
|
12 | 16 |
event.user.agents.find(event.payload['file_pointer']['agent_id']).get_io(event.payload['file_pointer']['file']) |
13 | 17 |
end |
14 | 18 |
|
19 |
+ def get_upload_io(event) |
|
20 |
+ Faraday::UploadIO.new(get_io(event), MIME::Types.type_for(File.basename(event.payload['file_pointer']['file'])).first.try(:content_type)) |
|
21 |
+ end |
|
22 |
+ |
|
15 | 23 |
def emitting_file_handling_agent_description |
16 | 24 |
@emitting_file_handling_agent_description ||= |
17 | 25 |
"This agent only emits a 'file pointer', not the data inside the files, the following agents can consume the created events: `#{receiving_file_handling_agents.join('`, `')}`. Read more about the concept in the [wiki](https://github.com/cantino/huginn/wiki/How-Huginn-works-with-files)." |
@@ -113,6 +113,7 @@ module WebRequestConcern |
||
113 | 113 |
unless boolify(interpolated['disable_redirect_follow']) |
114 | 114 |
builder.use FaradayMiddleware::FollowRedirects |
115 | 115 |
end |
116 |
+ builder.request :multipart |
|
116 | 117 |
builder.request :url_encoded |
117 | 118 |
|
118 | 119 |
if boolify(interpolated['disable_url_encoding']) |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class PostAgent < Agent |
3 | 3 |
include WebRequestConcern |
4 |
+ include FileHandling |
|
5 |
+ |
|
6 |
+ consumes_file_pointer! |
|
4 | 7 |
|
5 | 8 |
MIME_RE = /\A\w+\/.+\z/ |
6 | 9 |
|
@@ -8,38 +11,44 @@ module Agents |
||
8 | 11 |
no_bulk_receive! |
9 | 12 |
default_schedule "never" |
10 | 13 |
|
11 |
- description <<-MD |
|
12 |
- A Post Agent receives events from other agents (or runs periodically), merges those events with the [Liquid-interpolated](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) contents of `payload`, and sends the results as POST (or GET) requests to a specified url. To skip merging in the incoming event, but still send the interpolated payload, set `no_merge` to `true`. |
|
14 |
+ description do |
|
15 |
+ <<-MD |
|
16 |
+ A Post Agent receives events from other agents (or runs periodically), merges those events with the [Liquid-interpolated](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) contents of `payload`, and sends the results as POST (or GET) requests to a specified url. To skip merging in the incoming event, but still send the interpolated payload, set `no_merge` to `true`. |
|
13 | 17 |
|
14 |
- The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`). |
|
18 |
+ The `post_url` field must specify where you would like to send requests. Please include the URI scheme (`http` or `https`). |
|
15 | 19 |
|
16 |
- The `method` used can be any of `get`, `post`, `put`, `patch`, and `delete`. |
|
20 |
+ The `method` used can be any of `get`, `post`, `put`, `patch`, and `delete`. |
|
17 | 21 |
|
18 |
- By default, non-GETs will be sent with form encoding (`application/x-www-form-urlencoded`). |
|
22 |
+ By default, non-GETs will be sent with form encoding (`application/x-www-form-urlencoded`). |
|
19 | 23 |
|
20 |
- Change `content_type` to `json` to send JSON instead. |
|
24 |
+ Change `content_type` to `json` to send JSON instead. |
|
21 | 25 |
|
22 |
- Change `content_type` to `xml` to send XML, where the name of the root element may be specified using `xml_root`, defaulting to `post`. |
|
26 |
+ Change `content_type` to `xml` to send XML, where the name of the root element may be specified using `xml_root`, defaulting to `post`. |
|
23 | 27 |
|
24 |
- When `content_type` contains a [MIME](https://en.wikipedia.org/wiki/Media_type) type, and `payload` is a string, its interpolated value will be sent as a string in the HTTP request's body and the request's `Content-Type` HTTP header will be set to `content_type`. When `payload` is a string `no_merge` has to be set to `true`. |
|
28 |
+ When `content_type` contains a [MIME](https://en.wikipedia.org/wiki/Media_type) type, and `payload` is a string, its interpolated value will be sent as a string in the HTTP request's body and the request's `Content-Type` HTTP header will be set to `content_type`. When `payload` is a string `no_merge` has to be set to `true`. |
|
25 | 29 |
|
26 |
- If `emit_events` is set to `true`, the server response will be emitted as an Event and can be fed to a WebsiteAgent for parsing (using its `data_from_event` and `type` options). No data processing |
|
27 |
- will be attempted by this Agent, so the Event's "body" value will always be raw text. |
|
28 |
- The Event will also have a "headers" hash and a "status" integer value. |
|
29 |
- Set `event_headers_style` to one of the following values to normalize the keys of "headers" for downstream agents' convenience: |
|
30 |
+ If `emit_events` is set to `true`, the server response will be emitted as an Event and can be fed to a WebsiteAgent for parsing (using its `data_from_event` and `type` options). No data processing |
|
31 |
+ will be attempted by this Agent, so the Event's "body" value will always be raw text. |
|
32 |
+ The Event will also have a "headers" hash and a "status" integer value. |
|
33 |
+ Set `event_headers_style` to one of the following values to normalize the keys of "headers" for downstream agents' convenience: |
|
30 | 34 |
|
31 |
- * `capitalized` (default) - Header names are capitalized; e.g. "Content-Type" |
|
32 |
- * `downcased` - Header names are downcased; e.g. "content-type" |
|
33 |
- * `snakecased` - Header names are snakecased; e.g. "content_type" |
|
34 |
- * `raw` - Backward compatibility option to leave them unmodified from what the underlying HTTP library returns. |
|
35 |
+ * `capitalized` (default) - Header names are capitalized; e.g. "Content-Type" |
|
36 |
+ * `downcased` - Header names are downcased; e.g. "content-type" |
|
37 |
+ * `snakecased` - Header names are snakecased; e.g. "content_type" |
|
38 |
+ * `raw` - Backward compatibility option to leave them unmodified from what the underlying HTTP library returns. |
|
35 | 39 |
|
36 |
- Other Options: |
|
40 |
+ Other Options: |
|
37 | 41 |
|
38 |
- * `headers` - When present, it should be a hash of headers to send with the request. |
|
39 |
- * `basic_auth` - Specify HTTP basic auth parameters: `"username:password"`, or `["username", "password"]`. |
|
40 |
- * `disable_ssl_verification` - Set to `true` to disable ssl verification. |
|
41 |
- * `user_agent` - A custom User-Agent name (default: "Faraday v#{Faraday::VERSION}"). |
|
42 |
- MD |
|
42 |
+ * `headers` - When present, it should be a hash of headers to send with the request. |
|
43 |
+ * `basic_auth` - Specify HTTP basic auth parameters: `"username:password"`, or `["username", "password"]`. |
|
44 |
+ * `disable_ssl_verification` - Set to `true` to disable ssl verification. |
|
45 |
+ * `user_agent` - A custom User-Agent name (default: "Faraday v#{Faraday::VERSION}"). |
|
46 |
+ |
|
47 |
+ #{receiving_file_handling_agent_description} |
|
48 |
+ |
|
49 |
+ When receiving a `file_pointer` the request will be sent with multipart encoding (`multipart/form-data`) and `content_type` is ignored. `upload_key` can be used to specify the parameter in which the file will be sent, it defaults to `file`. |
|
50 |
+ MD |
|
51 |
+ end |
|
43 | 52 |
|
44 | 53 |
event_description <<-MD |
45 | 54 |
Events look like this: |
@@ -125,9 +134,9 @@ module Agents |
||
125 | 134 |
interpolate_with(event) do |
126 | 135 |
outgoing = interpolated['payload'].presence || {} |
127 | 136 |
if boolify(interpolated['no_merge']) |
128 |
- handle outgoing, event.payload, headers(interpolated[:headers]) |
|
137 |
+ handle outgoing, event, headers(interpolated[:headers]) |
|
129 | 138 |
else |
130 |
- handle outgoing.merge(event.payload), event.payload, headers(interpolated[:headers]) |
|
139 |
+ handle outgoing.merge(event.payload), event, headers(interpolated[:headers]) |
|
131 | 140 |
end |
132 | 141 |
end |
133 | 142 |
end |
@@ -162,8 +171,8 @@ module Agents |
||
162 | 171 |
} |
163 | 172 |
end |
164 | 173 |
|
165 |
- def handle(data, payload = {}, headers) |
|
166 |
- url = interpolated(payload)[:post_url] |
|
174 |
+ def handle(data, event = Event.new, headers) |
|
175 |
+ url = interpolated(event.payload)[:post_url] |
|
167 | 176 |
|
168 | 177 |
case method |
169 | 178 |
when 'get', 'delete' |
@@ -171,13 +180,21 @@ module Agents |
||
171 | 180 |
when 'post', 'put', 'patch' |
172 | 181 |
params = nil |
173 | 182 |
|
174 |
- case (content_type = interpolated(payload)['content_type']) |
|
183 |
+ content_type = |
|
184 |
+ if has_file_pointer?(event) |
|
185 |
+ data[interpolated(event.payload)['upload_key'].presence || 'file'] = get_upload_io(event) |
|
186 |
+ nil |
|
187 |
+ else |
|
188 |
+ interpolated(event.payload)['content_type'] |
|
189 |
+ end |
|
190 |
+ |
|
191 |
+ case content_type |
|
175 | 192 |
when 'json' |
176 | 193 |
headers['Content-Type'] = 'application/json; charset=utf-8' |
177 | 194 |
body = data.to_json |
178 | 195 |
when 'xml' |
179 | 196 |
headers['Content-Type'] = 'text/xml; charset=utf-8' |
180 |
- body = data.to_xml(root: (interpolated(payload)[:xml_root] || 'post')) |
|
197 |
+ body = data.to_xml(root: (interpolated(event.payload)[:xml_root] || 'post')) |
|
181 | 198 |
when MIME_RE |
182 | 199 |
headers['Content-Type'] = content_type |
183 | 200 |
body = data.to_s |
@@ -57,6 +57,11 @@ describe Agents::PostAgent do |
||
57 | 57 |
end |
58 | 58 |
|
59 | 59 |
it_behaves_like WebRequestConcern |
60 |
+ it_behaves_like 'FileHandlingConsumer' |
|
61 |
+ |
|
62 |
+ it 'renders the description markdown without errors' do |
|
63 |
+ expect { @checker.description }.not_to raise_error |
|
64 |
+ end |
|
60 | 65 |
|
61 | 66 |
describe "making requests" do |
62 | 67 |
it "can make requests of each type" do |
@@ -149,6 +154,19 @@ describe Agents::PostAgent do |
||
149 | 154 |
headers = @sent_requests[:post].first.headers |
150 | 155 |
expect(headers["Foo"]).to eq("a_variable") |
151 | 156 |
end |
157 |
+ |
|
158 |
+ it 'makes a multipart request when receiving a file_pointer' do |
|
159 |
+ WebMock.reset! |
|
160 |
+ stub_request(:post, "http://www.example.com/"). |
|
161 |
+ with(:body => "-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"default\"\r\n\r\nvalue\r\n-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"file\"; filename=\"local.path\"\r\nContent-Length: 8\r\nContent-Type: \r\nContent-Transfer-Encoding: binary\r\n\r\ntestdata\r\n-------------RubyMultipartPost--\r\n\r\n", |
|
162 |
+ :headers => {'Accept-Encoding'=>'gzip,deflate', 'Content-Length'=>'307', 'Content-Type'=>'multipart/form-data; boundary=-----------RubyMultipartPost', 'User-Agent'=>'Huginn - https://github.com/cantino/huginn'}). |
|
163 |
+ to_return(:status => 200, :body => "", :headers => {}) |
|
164 |
+ event = Event.new(payload: {file_pointer: {agent_id: 111, file: 'test'}}) |
|
165 |
+ io_mock = mock() |
|
166 |
+ mock(@checker).get_io(event) { StringIO.new("testdata") } |
|
167 |
+ @checker.options['no_merge'] = true |
|
168 |
+ @checker.receive([event]) |
|
169 |
+ end |
|
152 | 170 |
end |
153 | 171 |
|
154 | 172 |
describe "#check" do |
@@ -1,6 +1,8 @@ |
||
1 | 1 |
require 'rails_helper' |
2 | 2 |
|
3 | 3 |
shared_examples_for 'FileHandlingConsumer' do |
4 |
+ let(:event) { Event.new(user: @checker.user, payload: {'file_pointer' => {'file' => 'text.txt', 'agent_id' => @checker.id}}) } |
|
5 |
+ |
|
4 | 6 |
it 'returns a file pointer' do |
5 | 7 |
expect(@checker.get_file_pointer('testfile')).to eq(file_pointer: { file: "testfile", agent_id: @checker.id}) |
6 | 8 |
end |
@@ -9,8 +11,26 @@ shared_examples_for 'FileHandlingConsumer' do |
||
9 | 11 |
@checker2 = @checker.dup |
10 | 12 |
@checker2.user = users(:bob) |
11 | 13 |
@checker2.save! |
12 |
- expect(@checker2.user.id).not_to eq(@checker.user.id) |
|
13 |
- event = Event.new(user: @checker.user, payload: {'file_pointer' => {'file' => 'test', 'agent_id' => @checker2.id}}) |
|
14 |
+ event.payload['file_pointer']['agent_id'] = @checker2.id |
|
14 | 15 |
expect { @checker.get_io(event) }.to raise_error(ActiveRecord::RecordNotFound) |
15 | 16 |
end |
16 |
-end |
|
17 |
+ |
|
18 |
+ context '#has_file_pointer?' do |
|
19 |
+ it 'returns true if the event contains a file pointer' do |
|
20 |
+ expect(@checker.has_file_pointer?(event)).to be_truthy |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it 'returns false if the event does not contain a file pointer' do |
|
24 |
+ expect(@checker.has_file_pointer?(Event.new)).to be_falsy |
|
25 |
+ end |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ it '#get_upload_io returns a Faraday::UploadIO instance' do |
|
29 |
+ io_mock = mock() |
|
30 |
+ mock(@checker).get_io(event) { StringIO.new("testdata") } |
|
31 |
+ |
|
32 |
+ upload_io = @checker.get_upload_io(event) |
|
33 |
+ expect(upload_io).to be_a(Faraday::UploadIO) |
|
34 |
+ expect(upload_io.content_type).to eq('text/plain') |
|
35 |
+ end |
|
36 |
+end |